<?php
/**
- * Functions and constants to deal with grants
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
*/
use MediaWiki\MediaWikiServices;
DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
- // Decide when clients block on ChronologyProtector DB position writes
- $urlDomainDistance = (
- $request->wasPosted() &&
- $output->getRedirect() &&
- $lbFactory->hasOrMadeRecentMasterChanges( INF )
- ) ? self::getUrlDomainDistance( $output->getRedirect() ) : false;
+ // Should the client return, their request should observe the new ChronologyProtector
+ // DB positions. This request might be on a foreign wiki domain, so synchronously update
+ // the DB positions in all datacenters to be safe. If this output is not a redirect,
+ // then OutputPage::output() will be relatively slow, meaning that running it in
+ // $postCommitWork should help mask the latency of those updates.
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+ $strategy = 'cookie+sync';
$allowHeaders = !( $output->isDisabled() || headers_sent() );
- if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) {
- // OutputPage::output() will be fast; $postCommitWork will not be useful for
- // masking the latency of syncing DB positions accross all datacenters synchronously.
- // Instead, make use of the RTT time of the client follow redirects.
- $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
- $cpPosTime = microtime( true );
- // Client's next request should see 1+ positions with this DBMasterPos::asOf() time
- if ( $urlDomainDistance === 'local' && $allowHeaders ) {
- // Client will stay on this domain, so set an unobtrusive cookie
- $expires = time() + ChronologyProtector::POSITION_TTL;
- $options = [ 'prefix' => '' ];
- $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
- } else {
- // Cookies may not work across wiki domains, so use a URL parameter
- $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
- $output->getRedirect(),
- $cpPosTime
- );
- $output->redirect( $safeUrl );
+ if ( $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
+ // OutputPage::output() will be fast, so $postCommitWork is useless for masking
+ // the latency of synchronously updating the DB positions in all datacenters.
+ // Try to make use of the time the client spends following redirects instead.
+ $domainDistance = self::getUrlDomainDistance( $output->getRedirect() );
+ if ( $domainDistance === 'local' && $allowHeaders ) {
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+ $strategy = 'cookie'; // use same-domain cookie and keep the URL uncluttered
+ } elseif ( $domainDistance === 'remote' ) {
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+ $strategy = 'cookie+url'; // cross-domain cookie might not work
}
- } else {
- // OutputPage::output() is fairly slow; run it in $postCommitWork to mask
- // the latency of syncing DB positions accross all datacenters synchronously
- $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
- if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) && $allowHeaders ) {
- $cpPosTime = microtime( true );
- // Set a cookie in case the DB position store cannot sync accross datacenters.
- // This will at least cover the common case of the user staying on the domain.
+ }
+
+ // Record ChronologyProtector positions for DBs affected in this request at this point
+ $cpIndex = null;
+ $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex );
+ wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
+
+ if ( $cpIndex > 0 ) {
+ if ( $allowHeaders ) {
$expires = time() + ChronologyProtector::POSITION_TTL;
$options = [ 'prefix' => '' ];
- $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+ $request->response()->setCookie( 'cpPosIndex', $cpIndex, $expires, $options );
+ }
+
+ if ( $strategy === 'cookie+url' ) {
+ if ( $output->getRedirect() ) { // sanity
+ $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
+ $output->getRedirect(),
+ $cpIndex
+ );
+ $output->redirect( $safeUrl );
+ } else {
+ $e = new LogicException( "No redirect; cannot append cpPosIndex parameter." );
+ MWExceptionHandler::logException( $e );
+ }
}
}
- // Record ChronologyProtector positions for DBs affected in this request at this point
- $lbFactory->shutdown( $flags, $postCommitWork );
- wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
// Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
// POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
// Initialize the request object in $wgRequest
$wgRequest = RequestContext::getMain()->getRequest(); // BackCompat
// Set user IP/agent information for causal consistency purposes.
-// The cpPosTime cookie has no prefix and is set by MediaWiki::preOutputCommit().
-$cpPosTime = $wgRequest->getFloat( 'cpPosTime', $wgRequest->getCookie( 'cpPosTime', '' ) );
+// The cpPosIndex cookie has no prefix and is set by MediaWiki::preOutputCommit().
+$cpPosIndex = $wgRequest->getInt( 'cpPosIndex', (int)$wgRequest->getCookie( 'cpPosIndex', '' ) );
MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [
'IPAddress' => $wgRequest->getIP(),
'UserAgent' => $wgRequest->getHeader( 'User-Agent' ),
'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' ),
- 'ChronologyPositionTime' => $cpPosTime
+ 'ChronologyPositionIndex' => $cpPosIndex
] );
// Make sure that caching does not compromise the consistency improvements
-if ( $cpPosTime ) {
+if ( $cpPosIndex ) {
MediaWikiServices::getInstance()->getMainWANObjectCache()->useInterimHoldOffCaching( false );
}
-unset( $cpPosTime );
+unset( $cpPosIndex );
// Useful debug output
if ( $wgCommandLineMode ) {
$content = null;
$blobData = null;
- $blobFlags = '';
+ $blobFlags = null;
if ( is_object( $row ) ) {
// archive row
if ( isset( $row->old_text ) ) {
// this happens when the text-table gets joined directly, in the pre-1.30 schema
$blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
- $blobFlags = isset( $row->old_flags ) ? strval( $row->old_flags ) : '';
+ // Check against selects that might have not included old_flags
+ if ( !property_exists( $row, 'old_flags' ) ) {
+ throw new InvalidArgumentException( 'old_flags was not set in $row' );
+ }
+ $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags;
}
$mainSlotRow->slot_revision = intval( $row->rev_id );
$mainSlotRow->format_name = isset( $row['content_format'] )
? strval( $row['content_format'] ) : null;
$blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
- $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : '';
+ // XXX: If the flags field is not set then $blobFlags should be null so that no
+ // decoding will happen. An empty string will result in default decodings.
+ $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
// if we have a Content object, override mText and mContentModel
if ( !empty( $row['content'] ) ) {
*
* @param SlotRecord $slot The SlotRecord to load content for
* @param string|null $blobData The content blob, in the form indicated by $blobFlags
- * @param string $blobFlags Flags indicating how $blobData needs to be processed
+ * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
+ * null if no processing should happen.
* @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
* @param int $queryFlags
*
private function loadSlotContent(
SlotRecord $slot,
$blobData = null,
- $blobFlags = '',
+ $blobFlags = null,
$blobFormat = null,
$queryFlags = 0
) {
if ( $blobData !== null ) {
Assert::parameterType( 'string', $blobData, '$blobData' );
- Assert::parameterType( 'string', $blobFlags, '$blobFlags' );
+ Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
$cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
- $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
-
- if ( $data === false ) {
- throw new RevisionAccessException(
- "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
- );
+ if ( $blobFlags === null ) {
+ $data = $blobData;
+ } else {
+ $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
+ if ( $data === false ) {
+ throw new RevisionAccessException(
+ "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
+ );
+ }
}
+
} else {
$address = $slot->getAddress();
try {
$fld_flags = false, $fld_timestamp = false, $fld_user = false,
$fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
$fld_notificationtimestamp = false, $fld_userid = false,
- $fld_loginfo = false;
+ $fld_loginfo = false, $fld_tags;
/**
* @param ApiPageSet $resultPageSet
$this->fld_patrol = isset( $prop['patrol'] );
$this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
$this->fld_loginfo = isset( $prop['loginfo'] );
+ $this->fld_tags = isset( $prop['tags'] );
if ( $this->fld_patrol ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
if ( $this->fld_loginfo ) {
$includeFields[] = WatchedItemQueryService::INCLUDE_LOG_INFO;
}
+ if ( $this->fld_tags ) {
+ $includeFields[] = WatchedItemQueryService::INCLUDE_TAGS;
+ }
return $includeFields;
}
}
}
+ if ( $this->fld_tags ) {
+ if ( $recentChangeInfo['rc_tags'] ) {
+ $tags = explode( ',', $recentChangeInfo['rc_tags'] );
+ ApiResult::setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = [];
+ }
+ }
+
if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_RESTRICTED ) ) {
$vals['suppressed'] = true;
}
'sizes',
'notificationtimestamp',
'loginfo',
+ 'tags',
]
],
'show' => [
"apihelp-query+watchlist-paramvalue-prop-sizes": "Adds the old and new lengths of the page.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Adds timestamp of when the user was last notified about the edit.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Adds log information where appropriate.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Lists tags for the entry.",
"apihelp-query+watchlist-param-show": "Show only items that meet these criteria. For example, to see only minor edits done by logged-in users, set $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Which types of changes to show:",
"apihelp-query+watchlist-paramvalue-type-edit": "Regular page edits.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "{{doc-apihelp-paramvalue|query+watchlist|prop|sizes}}",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "{{doc-apihelp-paramvalue|query+watchlist|prop|notificationtimestamp}}",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "{{doc-apihelp-paramvalue|query+watchlist|prop|loginfo}}",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "{{doc-apihelp-paramvalue|query+watchlist|prop|tags}}",
"apihelp-query+watchlist-param-show": "{{doc-apihelp-param|query+watchlist|show}}",
"apihelp-query+watchlist-param-type": "{{doc-apihelp-param|query+watchlist|type}}",
"apihelp-query+watchlist-paramvalue-type-edit": "{{doc-apihelp-paramvalue|query+watchlist|type|edit}}",
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
use Wikimedia\Rdbms\IDatabase;
/**
- * An individual filter in a boolean group
+ * Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants)
*
* @since 1.29
*/
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
*/
public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) {
// Try #1: Non-encoded data URI
+
+ // Remove XML declaration, it's not needed with data URI usage
+ $contents = preg_replace( "/<\\?xml.*?\\?>/", '', $contents );
// The regular expression matches ASCII whitespace and printable characters.
if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
// Do not base64-encode non-binary files (sane SVGs).
$encoded = preg_replace( '/ {2,}/', ' ', $encoded );
// Remove leading and trailing spaces
$encoded = preg_replace( '/^ | $/', '', $encoded );
+
$uri = 'data:' . $type . ',' . $encoded;
if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
return $uri;
protected $key;
/** @var string Hash of client parameters */
protected $clientId;
- /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
- protected $waitForPosTime;
+ /** @var int|null Expected minimum index of the last write to the position store */
+ protected $waitForPosIndex;
/** @var int Max seconds to wait on positions to appear */
- protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
+ protected $waitForPosStoreTimeout = self::POS_STORE_WAIT_TIMEOUT;
/** @var bool Whether to no-op all method calls */
protected $enabled = true;
/** @var bool Whether to check and wait on positions */
/** @var int Seconds to store positions */
const POSITION_TTL = 60;
/** @var int Max time to wait for positions to appear */
- const POS_WAIT_TIMEOUT = 5;
+ const POS_STORE_WAIT_TIMEOUT = 5;
/**
* @param BagOStuff $store
- * @param array $client Map of (ip: <IP>, agent: <user-agent>)
- * @param float $posTime UNIX timestamp
+ * @param array[] $client Map of (ip: <IP>, agent: <user-agent>)
+ * @param int|null $posIndex Write counter index [optional]
* @since 1.27
*/
- public function __construct( BagOStuff $store, array $client, $posTime = null ) {
+ public function __construct( BagOStuff $store, array $client, $posIndex = null ) {
$this->store = $store;
$this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
$this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v1' );
- $this->waitForPosTime = $posTime;
+ $this->waitForPosIndex = $posIndex;
$this->logger = new NullLogger();
}
*
* @param callable|null $workCallback Work to do instead of waiting on syncing positions
* @param string $mode One of (sync, async); whether to wait on remote datacenters
+ * @param int|null &$cpIndex DB position key write counter; incremented on update
* @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
*/
- public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
+ public function shutdown( callable $workCallback = null, $mode = 'sync', &$cpIndex = null ) {
if ( !$this->enabled ) {
return [];
}
}
$ok = $store->set(
$this->key,
- self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
+ $this->mergePositions(
+ $store->get( $this->key ),
+ $this->shutdownPositions,
+ $cpIndex
+ ),
self::POSITION_TTL,
( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
);
$store->unlock( $this->key );
} else {
$ok = false;
+ $cpIndex = null; // nothing saved
}
if ( !$ok ) {
$this->initialized = true;
if ( $this->wait ) {
- // If there is an expectation to see master positions with a certain min
- // timestamp, then block until they appear, or until a timeout is reached.
- if ( $this->waitForPosTime > 0.0 ) {
+ // If there is an expectation to see master positions from a certain write
+ // index or higher, then block until it appears, or until a timeout is reached.
+ // Since the write index restarts each time the key is created, it is possible that
+ // a lagged store has a matching key write index. However, in that case, it should
+ // already be expired and thus treated as non-existing, maintaining correctness.
+ if ( $this->waitForPosIndex > 0 ) {
$data = null;
$loop = new WaitConditionLoop(
function () use ( &$data ) {
$data = $this->store->get( $this->key );
+ if ( !is_array( $data ) ) {
+ return WaitConditionLoop::CONDITION_CONTINUE; // not found yet
+ } elseif ( !isset( $data['writeIndex'] ) ) {
+ return WaitConditionLoop::CONDITION_REACHED; // b/c
+ }
- return ( self::minPosTime( $data ) >= $this->waitForPosTime )
+ return ( $data['writeIndex'] >= $this->waitForPosIndex )
? WaitConditionLoop::CONDITION_REACHED
: WaitConditionLoop::CONDITION_CONTINUE;
},
- $this->waitForPosTimeout
+ $this->waitForPosStoreTimeout
);
$result = $loop->invoke();
$waitedMs = $loop->getLastWaitTime() * 1e3;
if ( $result == $loop::CONDITION_REACHED ) {
- $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+ $msg = "expected and found pos index {$this->waitForPosIndex} ({$waitedMs}ms)";
$this->logger->debug( $msg );
} else {
- $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+ $msg = "expected but missed pos index {$this->waitForPosIndex} ({$waitedMs}ms)";
$this->logger->info( $msg );
}
} else {
}
}
- /**
- * @param array|bool $data
- * @return float|null
- */
- private static function minPosTime( $data ) {
- if ( !isset( $data['positions'] ) ) {
- return null;
- }
-
- $min = null;
- foreach ( $data['positions'] as $pos ) {
- if ( $pos instanceof DBMasterPos ) {
- $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
- }
- }
-
- return $min;
- }
-
/**
* @param array|bool $curValue
* @param DBMasterPos[] $shutdownPositions
+ * @param int|null &$cpIndex
* @return array
*/
- private static function mergePositions( $curValue, array $shutdownPositions ) {
+ protected function mergePositions( $curValue, array $shutdownPositions, &$cpIndex = null ) {
/** @var DBMasterPos[] $curPositions */
- if ( $curValue === false ) {
- $curPositions = $shutdownPositions;
- } else {
- $curPositions = $curValue['positions'];
- // Use the newest positions for each DB master
- foreach ( $shutdownPositions as $db => $pos ) {
- if (
- !isset( $curPositions[$db] ) ||
- !( $curPositions[$db] instanceof DBMasterPos ) ||
- $pos->asOfTime() > $curPositions[$db]->asOfTime()
- ) {
- $curPositions[$db] = $pos;
- }
+ $curPositions = isset( $curValue['positions'] ) ? $curValue['positions'] : [];
+ // Use the newest positions for each DB master
+ foreach ( $shutdownPositions as $db => $pos ) {
+ if (
+ !isset( $curPositions[$db] ) ||
+ !( $curPositions[$db] instanceof DBMasterPos ) ||
+ $pos->asOfTime() > $curPositions[$db]->asOfTime()
+ ) {
+ $curPositions[$db] = $pos;
}
}
- return [ 'positions' => $curPositions ];
+ $cpIndex = isset( $curValue['writeIndex'] ) ? $curValue['writeIndex'] : 0;
+
+ return [
+ 'positions' => $curPositions,
+ 'writeIndex' => ++$cpIndex
+ ];
}
}
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
namespace Wikimedia\Rdbms;
*
* @since 1.29
*
- * @license GPL-2.0+
* @author Addshore
*/
class ConnectionManager {
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
namespace Wikimedia\Rdbms;
*
* @since 1.29
*
- * @license GPL-2.0+
* @author Daniel Kinzler
* @author Addshore
*/
* Prepare all tracked load balancers for shutdown
* @param int $mode One of the class SHUTDOWN_* constants
* @param callable|null $workCallback Work to mask ChronologyProtector writes
+ * @param int|null &$cpIndex Position key write counter for ChronologyProtector
*/
public function shutdown(
- $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+ $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null, &$cpIndex = null
);
/**
public function setAgentName( $agent );
/**
- * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+ * Append ?cpPosIndex parameter to a URL for ChronologyProtector purposes if needed
*
* Note that unlike cookies, this works accross domains
*
* @param float $time UNIX timestamp just before shutdown() was called
* @return string
*/
- public function appendPreShutdownTimeAsQuery( $url, $time );
+ public function appendShutdownCPIndexAsQuery( $url, $time );
/**
* @param array $info Map of fields, including:
* - IPAddress : IP address
* - UserAgent : User-Agent HTTP header
* - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
- * - ChronologyPositionTime: timestamp used to get up-to-date DB positions for the agent
+ * - ChronologyPositionIndex: timestamp used to get up-to-date DB positions for the agent
*/
public function setRequestInfo( array $info );
}
'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ChronologyProtection' => 'true',
- 'ChronologyPositionTime' => isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
+ 'ChronologyPositionIndex' => isset( $_GET['cpPosIndex'] ) ? $_GET['cpPosIndex'] : null
];
$this->cliMode = isset( $conf['cliMode'] ) ? $conf['cliMode'] : PHP_SAPI === 'cli';
}
public function shutdown(
- $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+ $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null, &$cpIndex = null
) {
$chronProt = $this->getChronologyProtector();
if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
- $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
+ $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync', $cpIndex );
} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
- $this->shutdownChronologyProtector( $chronProt, null, 'async' );
+ $this->shutdownChronologyProtector( $chronProt, null, 'async', $cpIndex );
}
$this->commitMasterChanges( __METHOD__ ); // sanity
'ip' => $this->requestInfo['IPAddress'],
'agent' => $this->requestInfo['UserAgent'],
],
- $this->requestInfo['ChronologyPositionTime']
+ $this->requestInfo['ChronologyPositionIndex']
);
$this->chronProt->setLogger( $this->replLogger );
* @param ChronologyProtector $cp
* @param callable|null $workCallback Work to do instead of waiting on syncing positions
* @param string $mode One of (sync, async); whether to wait on remote datacenters
+ * @param int|null &$cpIndex DB position key write counter; incremented on update
*/
protected function shutdownChronologyProtector(
- ChronologyProtector $cp, $workCallback, $mode
+ ChronologyProtector $cp, $workCallback, $mode, &$cpIndex = null
) {
// Record all the master positions needed
$this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
} );
// Write them to the persistent stash. Try to do something useful by running $work
// while ChronologyProtector waits for the stash write to replicate to all DCs.
- $unsavedPositions = $cp->shutdown( $workCallback, $mode );
+ $unsavedPositions = $cp->shutdown( $workCallback, $mode, $cpIndex );
if ( $unsavedPositions && $workCallback ) {
// Invoke callback in case it did not cache the result yet
$workCallback(); // work now to block for less time in waitForAll()
$this->agent = $agent;
}
- public function appendPreShutdownTimeAsQuery( $url, $time ) {
+ public function appendShutdownCPIndexAsQuery( $url, $index ) {
$usedCluster = 0;
$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
$usedCluster |= ( $lb->getServerCount() > 1 );
return $url; // no master/replica clusters touched
}
- return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+ return strpos( $url, '?' ) === false ? "$url?cpPosIndex=$index" : "$url&cpPosIndex=$index";
}
public function setRequestInfo( array $info ) {
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
use Wikimedia\Http\HttpAcceptParser;
use Wikimedia\Http\HttpAcceptNegotiator;
/**
* Request handler implementing a data interface for mediawiki pages.
*
- * @license GPL-2.0+
* @author Daniel Kinzler
* @author Amir Sarabadanai
*/
-
class PageDataRequestHandler {
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL-2.0+
* @author Kunal Mehta <legoktm@member.fsf.org>
*/
namespace MediaWiki\Linker;
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL-2.0+
* @author Kunal Mehta <legoktm@member.fsf.org>
*/
namespace MediaWiki\Linker;
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Addshore
*/
namespace MediaWiki\Linker;
<?php
+/**
+ * Special page to act as an endpoint for accessing raw page data.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
/**
* Special page to act as an endpoint for accessing raw page data.
* The web server should generally be configured to make this accessible via a canonical URL/URI,
* such as <http://my.domain.org/data/main/Foo>.
*
- * @license GPL-2.0+
+ * @class
+ * @ingroup SpecialPage
*/
class SpecialPageData extends SpecialPage {
'name' => 'namespace',
'id' => 'namespace',
'cssclass' => 'namespaceselector',
- 'selected' => $namespace,
'all' => '',
'label' => $this->msg( 'namespace' )->text(),
],
'label' => $this->msg( 'protectedpages-indef' )->text(),
'name' => 'indefonly',
'id' => 'indefonly',
- 'value' => $indefOnly
],
'cascadecheck' => [
'type' => 'check',
'label' => $this->msg( 'protectedpages-cascade' )->text(),
'name' => 'cascadeonly',
'id' => 'cascadeonly',
- 'value' => $cascadeOnly
],
'redirectcheck' => [
'type' => 'check',
'label' => $this->msg( 'protectedpages-noredirect' )->text(),
'name' => 'noredirect',
'id' => 'noredirect',
- 'value' => $noRedirect,
],
'sizelimit' => [
'class' => 'HTMLSizeFilterField',
return [
'type' => 'select',
'options' => $options,
- 'value' => $pr_type,
'label' => $this->msg( 'restriction-type' )->text(),
'name' => $this->IdType,
'id' => $this->IdType,
return [
'type' => 'select',
'options' => $options,
- 'value' => $pr_level,
'label' => $this->msg( 'restriction-level' )->text(),
'name' => $this->IdLevel,
'id' => $this->IdLevel
}
$link = $this->getLinkRenderer()->makeLink( $title );
- $description_items = [];
// Messages: restriction-level-sysop, restriction-level-autoconfirmed
- $protType = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped();
- $description_items[] = $protType;
+ $description = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped();
$lang = $this->getLanguage();
$expiry = strlen( $row->pt_expiry ) ?
$lang->formatExpiry( $row->pt_expiry, TS_MW ) :
if ( $expiry !== 'infinity' ) {
$user = $this->getUser();
- $description_items[] = $this->msg(
+ $description .= $this->msg( 'comma-separator' )->escaped() . $this->msg(
'protect-expiring-local',
$lang->userTimeAndDate( $expiry, $user ),
$lang->userDate( $expiry, $user ),
)->escaped();
}
- // @todo i18n: This should use a comma separator instead of a hard coded comma, right?
- return '<li>' . $lang->specialList( $link, implode( $description_items, ', ' ) ) . "</li>\n";
+ return '<li>' . $lang->specialList( $link, $description ) . "</li>\n";
}
/**
* @ingroup SpecialPage
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
<?php
/**
- * A codec for %MediaWiki page titles.
+ * A codec for MediaWiki page titles.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Interwiki\InterwikiLookup;
use MediaWiki\Linker\LinkTarget;
/**
- * A codec for %MediaWiki page titles.
+ * A codec for MediaWiki page titles.
*
* @note Normalization and validation is applied while parsing, not when formatting.
* It's possible to construct a TitleValue with an invalid title, and use MediaWikiTitleCodec
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
<?php
/**
- * A title formatter service for %MediaWiki.
+ * A title formatter service for MediaWiki.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Linker\LinkTarget;
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
<?php
/**
- * Representation of a page title within %MediaWiki.
+ * Representation of a page title within MediaWiki.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Linker\LinkTarget;
use Wikimedia\Assert\Assert;
/**
- * Represents a page (or page fragment) title within %MediaWiki.
+ * Represents a page (or page fragment) title within MediaWiki.
*
* @note In contrast to Title, this is designed to be a plain value object. That is,
* it is immutable, does not use global state, and causes no side effects.
const INCLUDE_PATROL_INFO = 'patrol';
const INCLUDE_SIZES = 'sizes';
const INCLUDE_LOG_INFO = 'loginfo';
+ const INCLUDE_TAGS = 'tags';
// FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
// ApiQueryWatchlistRaw classes) and should not be changed.
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
$tables += $this->getCommentStore()->getJoin()['tables'];
}
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ $tables[] = 'tag_summary';
+ }
return $tables;
}
if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
$fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
}
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
+ $fields['rc_tags'] = 'ts_tags';
+ }
return $fields;
}
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
$joinConds += $this->getCommentStore()->getJoin()['joins'];
}
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ $joinConds['tag_summary'] = [ 'LEFT JOIN', [ 'rc_id=ts_rc_id' ] ];
+ }
return $joinConds;
}
* @return string
*/
function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
+ # Check if there is no need to truncate
+ if ( strlen( $string ) <= abs( $length ) ) {
+ return $string; // no need to truncate
+ }
# Use the localized ellipsis character
if ( $ellipsis == '...' ) {
$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
}
- # Check if there is no need to truncate
if ( $length == 0 ) {
return $ellipsis; // convention
- } elseif ( strlen( $string ) <= abs( $length ) ) {
- return $string; // no need to truncate
}
$stringOriginal = $string;
# If ellipsis length is >= $length then we can't apply $adjustLength
"rcfilters-activefilters": "Активни филтри",
"rcfilters-advancedfilters": "Разширени филтри",
"rcfilters-limit-title": "Резултати за показване",
- "rcfilters-limit-and-date-label": "{{PLURAL:$1|промяна|$1 промени}}, $2",
+ "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|промяна|промени}}, $2",
"rcfilters-date-popup-title": "Период за търсене",
"rcfilters-days-title": "Последните дни",
"rcfilters-hours-title": "Последните часове",
"search-redirect": "($1 से अनुप्रेषित)",
"search-section": "(खंड $1)",
"search-category": "(श्रेणी $1)",
+ "search-file-match": "(फाइल सामग्री से मैच करत बा)",
"search-suggest": "का राउर मतलब बा: $1",
"search-rewritten": "$1 खातिर रिजल्ट। एकरे जगह $2 खातिर खोज करीं।",
"search-interwiki-caption": "साथी प्रोजेक्ट सभ से रिजल्ट",
"filehist-help": "ओ समय ई फाइल कइसन लउके ई देखे खातिर कौनों तारीख/समय पर क्लिक करीं।",
"filehist-deleteall": "सब मिटाईं",
"filehist-deleteone": "मिटाईं",
+ "filehist-revert": "वापस लीं",
"filehist-current": "वर्तमान",
"filehist-datetime": "तारीख/समय",
"filehist-thumb": "चिप्पी रूप",
"pageswithprop-prop": "प्रापर्टी नाँव:",
"pageswithprop-submit": "जाईं",
"doubleredirects": "दोहरा पुननिर्देशित पन्ना",
+ "double-redirect-fixer": "अनुप्रेषण सुधारक",
"brokenredirects": "टूटल पुनर्निर्देशन पन्ना",
"brokenredirects-edit": "संपादन",
"brokenredirects-delete": "मिटाईं",
"booksources": "किताबी स्रोत",
"booksources-search-legend": "किताबी स्रोत के खोज",
"booksources-search": "खोज",
+ "specialloguserlabel": "जेकरे द्वारा कइल गइल:",
+ "speciallogtitlelabel": "टारगेट (टाइटिल या {{ns:user}}:प्रयोगकर्ता खाती प्रयोगकर्तानाँव):",
"log": "सगरी लॉग",
"all-logs-page": "सगरी पब्लिक लॉग",
"allpages": "सगरी पन्ना",
"protectlogpage": "सुरक्षा लॉग",
"protectlogtext": "नीचे पन्ना सुरक्षा में भइल बदलावकुल के सूची बा।\nहाल में सुरक्षित पन्नन के सूची खातिर [[Special:ProtectedPages|सुरक्षित पन्नन के सूची]] देखीं।",
"protectedarticle": "\"[[$1]]\" सुरक्षित कइल गइल",
+ "modifiedarticleprotection": "\"[[$1]]\" खातिर सुरक्षा स्तर बदलल गइल",
"protect-default": "सगरी प्रयोगकर्ता लोग के एलाऊ करीं",
"restriction-edit": "संपादन करीं",
"restriction-move": "स्थानांतरण",
"contribslink": "योगदान",
"blocklogpage": "निष्क्रिय खाता",
"blocklogentry": "[[$1]] के ब्लॉक कइल गइल, समाप्ती के अवधि $2 $3",
+ "block-log-flags-nocreate": "खाता निर्माण सक्षम नइखे",
+ "proxyblocker": "प्रॉक्सी ब्लॉककर्ता",
"movepagebtn": "पन्ना स्थांतरण करीं",
"movelogpage": "स्थानांतरण लॉग",
"revertmove": "पिछलका स्थिति",
"importcantopen": "आयात फाइल के खोले में असमर्थ",
"importbadinterwiki": "खराब इंटरविकि कड़ी",
"importsuccess": "आयात पूरा भइल!",
+ "importlogpage": "आयात के लॉग",
"import-logentry-upload-detail": "$1 {{PLURAL:$1|संशोधन|संशोधनसभ}} लावल गइल",
"tooltip-pt-userpage": "{{GENDER:|राउर सदस्य}} पन्ना",
"tooltip-pt-mytalk": "{{GENDER:|राउर}} बातचीत पन्ना",
"tooltip-ca-nstab-special": "ई एगो खास पन्ना ह, एकर संपादन ना हो सकेला",
"tooltip-ca-nstab-project": "प्रोजेक्ट पन्ना देखीं",
"tooltip-ca-nstab-image": "फाइल के पन्ना देखीं",
+ "tooltip-ca-nstab-mediawiki": "सिस्टम सनेसा देखीं",
"tooltip-ca-nstab-template": "टेम्पलेट देखीं",
"tooltip-ca-nstab-category": "श्रेणी के पन्ना देखीं",
+ "tooltip-minoredit": "एकरा के छोट संपादन चिन्हित करीं",
"tooltip-save": "जवन बदलाव कइलीं ओकरा के सहेजीं",
"tooltip-preview": "जवन बदलाव कइलीं ओकर झलक देखीं। सहेजे से पहिले एकर इस्तेमाल करे के आगरह बा।",
"tooltip-diff": "देखीं कि पाठ में आप का बदलाव कइले बानी",
"tooltip-compareselectedversions": "एह पन्ना के चुनल गइल दू गो रिवीजन सभ में अंतर देखीं",
+ "tooltip-watch": "ए पन्ना के अपनी धियानसूची में जोड़ीं",
"tooltip-rollback": "\"रोलबैक\" एह पन्ना पर सभसे अंतिम संपादन करे वाला संपादक के कइल बदलाव(सभ) एकही क्लिक में वापस लवटा देला",
"tooltip-undo": "\"वापस लीं\" ए संपादन के पलट देला आ संपादन फार्म के झलक देखावे वाला मोड में खोलेला। ई छोट सारांश में कारण जोड़े के मोका देला।",
"tooltip-summary": "संछेप में एगो सारांश लिखीं",
"simpleantispam-label": "स्पैम-बिरोधी रोक (Anti-spam check)\nएके <strong>मत</strong> भरीं!",
+ "pageinfo-title": "\"$1\" खातिर जानकारी",
+ "pageinfo-header-basic": "बेसिक जानकारी",
"pageinfo-header-edits": "संपादन इतिहास",
+ "pageinfo-header-restrictions": "पन्ना सुरक्षा",
+ "pageinfo-display-title": "टाइटिल जवन देखाई पड़ी",
+ "pageinfo-default-sort": "डिफॉल्ट सॉर्ट कुंजी",
"pageinfo-length": "पन्ना लंबाई (बाइट में)",
"pageinfo-article-id": "पन्ना आइडी",
+ "pageinfo-language": "पन्ना के सामग्री के भाषा",
+ "pageinfo-content-model": "पन्ना सामग्री के मॉडल",
+ "pageinfo-robot-policy": "रोबोट द्वारा इंडेक्सिंग",
+ "pageinfo-robot-index": "एलाऊ बा",
+ "pageinfo-watchers": "पन्ना पर धियान रखे वाला लोग के संख्या",
+ "pageinfo-few-watchers": "$1 से कम धियान रखे {{PLURAL:$1|वाला|वाला लोग}}",
+ "pageinfo-redirects-name": "एह पन्ना पर आवे वाला अनुप्रेषणन के संख्या",
"pageinfo-subpages-name": "एह पन्ना के उपपन्ना संख्या",
"pageinfo-firstuser": "पन्ना बनावेवाला",
"pageinfo-firsttime": "पन्ना बनावे के तारीख",
"pageinfo-lasttime": "सभसे नया संपादन के तारीख",
"pageinfo-edits": "कुल संपादन गिनती",
"pageinfo-authors": "कुल अलग-अलग संपादकन के गिनती",
+ "pageinfo-recent-edits": "हाल के संपादन संख्या (पछिला $1 में)",
+ "pageinfo-recent-authors": "हाल के बिभिन्न संपादक लोग के संख्या",
"pageinfo-magic-words": "जादुई शब्द {{{{PLURAL:$1||शब्द|शब्द}}}} ($1)",
"pageinfo-toolboxlink": "पन्ना से जुड़ल जानकारी",
+ "pageinfo-contentpage": "सामग्री पन्ना के रूप में गिनती वाला",
+ "pageinfo-contentpage-yes": "हँऽ",
+ "patrol-log-page": "जाँच के लॉग",
"previousdiff": "← पुरान संपादन",
"nextdiff": "नया संपादन →",
+ "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|पन्ना}}",
"file-info-size": "$1 × $2 पिक्सेल, फाइल साइज: $3, MIME टाइप: $4",
"file-nohires": "ए से उच्च गुणवत्ता उपलब्ध नइखे।",
"svg-long-desc": "एसवीजी फाइल, नॉमिनली $1 x $2 पिक्सल, फाइल के आकार: $3",
"autoredircomment": "पन्ना [[$1]] पर अनुप्रेषित कइल गइल",
"signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|बात करीं]])",
"version-no-ext-name": "[अज्ञात नाम]",
+ "redirect-summary": "ई बिसेस पन्ना कौनों फाइल (जबकि फाइलनाँव दिहल होखे), पन्ना (रिवीजन आइडी या पन्ना आइडी दिहल होखे) या, प्रयोगकर्ता पन्ना (प्रयोगकर्ता आइडी दिहल होखे), या लॉग एंट्री (लॉग आइडी दिहल होखे) अनुप्रेषित करी। इस्तेमाल: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], या [[{{#Special:Redirect}}/logid/186]].",
"fileduplicatesearch": "नकल प्रति फाइल खोजीं",
"specialpages": "खास पन्ना",
"specialpages-group-maintenance": "रखरखाव रिपोर्ट",
"specialpages-group-wiki": "डेटा अउर औजार",
"tag-filter": "[[Special:Tags|टैग]] छननी:",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|टैग|टैग कुल}}]]: $2)",
+ "tags-active-yes": "हँऽ",
+ "tags-active-no": "ना",
+ "tags-hitcount": "$1 {{PLURAL:$1|बदलाव}}",
"logentry-delete-delete": "$1 द्वारा पन्ना $3 {{GENDER:$2|हटा}} दिहल गइल",
+ "logentry-delete-revision": "$1 पन्ना $3 पर {{PLURAL:$5|रिवीजन|$5 रिवीजन सभ}} के विजिबिलिटी {{GENDER:$2|बदललें|बदलली}}: $4",
"revdelete-restricted": "प्रबंधक पर प्रतिबंध लागू",
"revdelete-unrestricted": "प्रबंधक पर से प्रतिबंध समाप्त",
"logentry-move-move": "$1 पन्ना $3 के $4 पर {{GENDER:$2|स्थानांतरण कइलें}}",
+ "logentry-move-move-noredirect": "$1 {{GENDER:$2|द्वारा}} बिना अनुप्रेषण छोड़ले $3 पन्ना के $4 पर स्थानांतरण कइल गइल",
+ "logentry-move-move_redir": "$1 पन्ना $3 के $4 पर अनुप्रेषण के ऊपर स्थानांतरण {{GENDER:$2|कइलें|कइली}}",
+ "logentry-patrol-patrol-auto": "पन्ना $3 के रिवीजन $4 $1 द्वारा ऑटोमेटिक {{GENDER:$2|जाँचल चिन्हित कइल गइल}}",
"logentry-newusers-create": "खाता $1 {{GENDER:$2|बनावल गइल}}",
"logentry-upload-upload": "$1 {{GENDER:$2|अपलोड कइलें}} $3",
"searchsuggest-search": "{{SITENAME}} में खोजीं",
- "duration-days": "$1 दिन",
+ "duration-days": "$1 {{PLURAL:$1|दिन}}",
"expandtemplates": "टेम्पलेट बिस्तार",
"mediastatistics": "मीडिया सांख्यिकी"
}
"rcfilters-activefilters": "Aktivní filtry",
"rcfilters-advancedfilters": "Pokročilé filtry",
"rcfilters-limit-title": "Zobrazené výsledky",
- "rcfilters-limit-and-date-label": "{{PLURAL:$1|změna|$1 změny|$1 změn}}, $2",
+ "rcfilters-limit-and-date-label": "{{PLURAL:$1|Jedna změna|$1 změny|$1 změn}}, $2",
"rcfilters-date-popup-title": "Hledané časové období",
"rcfilters-days-title": "Poslední dny",
"rcfilters-hours-title": "Poslední hodiny",
"botpasswords-insert-failed": "Botin nimen \"$1\" lisääminen epäonnsitui. Onko se jo lisätty?",
"botpasswords-update-failed": "Botin nimen \"$1\" päivittäminen epäonnistui. Onko se poistettu?",
"botpasswords-created-title": "Bottisalasana luotu",
- "botpasswords-created-body": "Bottisalasana käyttäjän \"$2\" bottinimelle \"$1\" luotiin.",
+ "botpasswords-created-body": "Bottisalasana {{GENDER:$2|käyttäjän}} \"$2\" bottinimelle \"$1\" luotiin.",
"botpasswords-updated-title": "Bottisalasana päivitetty",
- "botpasswords-updated-body": "Bottisalasana käyttäjän \"$2\" bottinimelle \"$1\" päivitettiin.",
+ "botpasswords-updated-body": "Bottisalasana {{GENDER:$2|käyttäjän}} \"$2\" bottinimelle \"$1\" päivitettiin.",
"botpasswords-deleted-title": "Bottisalasana poistettu",
- "botpasswords-deleted-body": "Bottisalasana käyttäjän \"$2\" bottinimelle \"$1\" poistettiin.",
+ "botpasswords-deleted-body": "Bottisalasana {{GENDER:$2|käyttäjän}} \"$2\" bottinimelle \"$1\" poistettiin.",
"botpasswords-newpassword": "Uusi salasana kirjautumiseen käyttäjällä <strong>$1</strong> on <strong>$2</strong>. <em>Säilytä tämä myöhempää käyttöä varten.</em> <br> (Vanhoilla boteilla, jotka vaativat kirjautumisnimen olevan sama kuin lopullinen käyttäjänimi, voit myös käyttää nimeä <strong>$3</strong> ja salasanaa <strong>$4</strong>.)",
"botpasswords-no-provider": "BotPasswordsSessionProvider ei ole saatavilla.",
"botpasswords-restriction-failed": "Bottisalasanan rajoitukset estävät tämän sisäänkirjautumisen.",
"rcfilters-activefilters": "Aktiiviset suodattimet",
"rcfilters-advancedfilters": "Kehittyneet suodattimet",
"rcfilters-limit-title": "Näytettävät tulokset",
- "rcfilters-limit-and-date-label": "{{PLURAL:$1|muutos|$1 muutosta}}, $2",
+ "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|muutos|muutosta}}, $2",
"rcfilters-date-popup-title": "Aikajakso hakua varten",
"rcfilters-days-title": "Viimeisimmät päivät",
"rcfilters-hours-title": "Viimeisimmät tunnit",
"import-mapping-namespace": "Tuonti nimiavaruuteen:",
"import-mapping-subpage": "Tuonti seuraavan sivun alasivuiksi:",
"import-upload-filename": "Tiedostonimi:",
+ "import-upload-username-prefix": "Interwiki-etuliite:",
"import-comment": "Kommentti:",
"importtext": "Vie sivuja lähdewikistä käyttäen [[Special:Export|vientityökalua]].\nTallenna tiedot koneellesi ja tuo ne tällä sivulla.",
"importstart": "Tuodaan sivuja...",
"version-poweredby-others": "muut",
"version-poweredby-translators": "translatewiki.net-kääntäjät",
"version-credits-summary": "Haluamme kiittäen mainita seuraavat henkilöt, jotka ovat osallistuneet [[Special:Version|MediaWiki-ohjelmiston]] kehittämiseen.",
- "version-license-info": "MediaWiki on vapaa ohjelmisto – voit levittää sitä ja/tai muokata sitä Free Software Foundationin GNU General Public Licensen ehdoilla, joko version 2 tai halutessasi minkä tahansa myöhemmän version mukaisesti.\n\nMediaWikiä levitetään siinä toivossa, että se olisi hyödyllinen, mutta ilman mitään takuuta; ilman edes hiljaista takuuta kaupallisesti hyväksyttävästä laadusta tai soveltuvuudesta tiettyyn tarkoitukseen. Katso GPL-lisenssistä lisää yksityiskohtia.\n\nSinun olisi pitänyt saada [{{SERVER}}{{SCRIPTPATH}}/COPYING kopio GNU General Public Licensestä] tämän ohjelman mukana. Jos et saanut kopiota, kirjoita siitä osoitteeseen Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA tai [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lue se Internetissä].",
+ "version-license-info": "MediaWiki on vapaa ohjelmisto; voit levittää sitä ja/tai muokata sitä Free Software Foundationin GNU General Public Licensen ehdoilla, joko version 2 tai halutessasi minkä tahansa myöhemmän version mukaisesti.\n\nMediaWikiä levitetään siinä toivossa, että se olisi hyödyllinen, mutta ilman mitään takuuta; ilman edes hiljaista takuuta kaupallisesti hyväksyttävästä laadusta tai soveltuvuudesta tiettyyn tarkoitukseen. Katso GPL-lisenssistä lisää yksityiskohtia.\n\nSinun olisi pitänyt saada [{{SERVER}}{{SCRIPTPATH}}/COPYING kopio GNU General Public Licensestä] tämän ohjelman mukana. Jos et saanut kopiota, kirjoita siitä osoitteeseen Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA tai [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lue se Internetissä].",
"version-software": "Asennettu ohjelmisto",
"version-software-product": "Tuote",
"version-software-version": "Versio",
"expandtemplates": "Laajenna mallineet",
"expand_templates_intro": "Tämä toimintosivu ottaa syötteeksi tekstiä ja laajentaa kaikki siinä olevat mallineet rekursiivisesti.\nSe myös laajentaa tuetut parserifunktiot kuten\n<code><nowiki>{{</nowiki>#language:...}}</code> ja -muuttujat kuten\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nKäytännössä se laajentaa melkein kaiken, joka on kaksoisaaltosulkeiden sisällä.",
"expand_templates_title": "Otsikko (esimerkiksi muuttujaa {{FULLPAGENAME}} varten)",
- "expand_templates_input": "Teksti",
+ "expand_templates_input": "Syötä wikiteksti:",
"expand_templates_output": "Tulos",
"expand_templates_xml_output": "XML-tuloste",
"expand_templates_html_output": "Raaka HTML-koodi",
"expand_templates_preview": "Esikatselu",
"expand_templates_preview_fail_html": "<em>Koska sivustolla {{SITENAME}} on käytössä suodattamaton HTML-koodi ja koska istunnon tiedot ovat kadonneet, esikatselu on piilotettu JavaScript-hyökkäyksien torjumiseksi.</em>\n\n<strong>Jos yritit esikatsella sivua, yritä uudestaan.</strong>\nJos esikatselu ei vieläkään toimi, yritä [[Special:UserLogout|kirjautua ulos]] ja sitten kirjautua uudestaan sisään. Tarkista myös, että selaimesi sallii evästeet tältä sivustolta.",
"expand_templates_preview_fail_html_anon": "<em>Koska sivustolla {{SITENAME}} on käytössä puhdas HTML-koodi ja koska et ole kirjautunut sisään, esikatselu on piilotettu JavaScript-hyökkäyksien torjumiseksi.</em>\n\n<strong>Jos olet oikealla asialla, [[Special:UserLogin|kirjaudu sisään]] ja yritä uudestaan.</strong>",
- "expand_templates_input_missing": "Sinun on annettava edes jotakin tekstiä syötteeksi.",
+ "expand_templates_input_missing": "Sinun on annettava ainakin jotakin wikitekstiä syötteeksi.",
"pagelanguage": "Sivun kielen vaihto",
"pagelang-name": "Sivu",
"pagelang-language": "Kieli",
"restrictionsfield-label": "Sallitut IP-alueet:",
"revid": "versio $1",
"pageid": "sivun tunnistenumero $1",
+ "rawhtml-notallowed": "<html> komentoa ei voida käyttää normaalien sivujen ulkopuolella.",
"gotointerwiki": "Lähdössä {{GRAMMAR:elative|{{SITENAME}}}}",
"gotointerwiki-invalid": "Annettu otsikko on virheellinen.",
"gotointerwiki-external": "Olet lähdössä {{GRAMMAR:elative|{{SITENAME}}}} toiselle sivustolle [[$2]].\n\n'''[$1 Jatka osoitteeseen $1]'''",
"rcfilters-filter-previousrevision-label": "Ekki nýjasta útgáfa",
"rcfilters-filter-previousrevision-description": "Allar breytingar nema sú nýjasta.",
"rcfilters-filter-excluded": "Útilokað",
+ "rcfilters-tag-prefix-namespace-inverted": "<strong>:ekki</strong> $1",
"rcfilters-exclude-button-off": "Útiloka val",
+ "rcfilters-exclude-button-on": "Útiloka valið",
"rcfilters-view-tags": "Merktar breytingar",
"rcfilters-view-namespaces-tooltip": "Sía niðurstöður eftir nafnrými",
"rcfilters-view-tags-tooltip": "Sía niðurstöður með breytingarmerkjum",
"rcfilters-liveupdates-button-title-off": "Sýna nýjar breytingar um leið og þær gerast",
"rcfilters-watchlist-markseen-button": "Merkja allar breytingar sem skoðaðar",
"rcfilters-watchlist-edit-watchlist-button": "Breyta þínum lista yfir vaktaðar síður",
+ "rcfilters-target-page-placeholder": "Settu inn síðuheiti (eða flokk)",
"rcnotefrom": "Að neðan {{PLURAL:$5|er breyting síðan|eru breytingar síðan}} <strong>$3, $4</strong> (allt að <strong>$1</strong> sýndar).",
"rclistfromreset": "Endurstilla dagsetningarval",
"rclistfrom": "Sýna breytingar frá og með $3 $2",
"uploadstash-thumbnail": "skoða smámynd",
"uploadstash-bad-path": "Slóðin er ekki til.",
"uploadstash-bad-path-invalid": "Slóðin er ógild.",
+ "uploadstash-bad-path-unknown-type": "Óþekkt gerð \"$1\".",
"invalid-chunk-offset": "Ógild raðbreyting bunka",
"img-auth-accessdenied": "Aðgangur óheimill",
"img-auth-nopathinfo": "PATH_INFO vantar.\nBiðlarinn þínn er ekki stilltur til að gefa upp þessar upplýsingar.\nÞær mega vera CGI-byggðar og mega ekki styðja img_auth.\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization",
"apisandbox-alert-field": "Gildi þessa reits er ekki leyfilegt.",
"apisandbox-continue": "Halda áfram",
"apisandbox-continue-clear": "Hreinsa",
+ "apisandbox-multivalue-all-namespaces": "$1 (öll nafnarými)",
+ "apisandbox-multivalue-all-values": "$1 (öll gildi)",
"booksources": "Bókaleit",
"booksources-search-legend": "Leita að bókaheimildum",
"booksources-isbn": "ISBN:",
"pageinfo-category-subcats": "Fjöldi undirflokka",
"pageinfo-category-files": "Fjöldi skráa",
"pageinfo-user-id": "Notandanúmer",
+ "pageinfo-file-hash": "Tætigildi (hash)",
"markaspatrolleddiff": "Merkja sem yfirfarið",
"markaspatrolledtext": "Merkja þessa síðu sem yfirfarna",
"markaspatrolledtext-file": "Merkja þessa útgáfu skrár sem yfirfarna",
"tag-filter": "[[Special:Tags|Merkja]]sía:",
"tag-filter-submit": "Sía",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Merki}}]]: $2)",
+ "tag-mw-replace": "Skipt út",
+ "tag-mw-undo": "Afturkalla",
"tags-title": "Merki",
"tags-intro": "Þessi síða sýnir merkin sem hugbúnaðurinn gæti merkt breytingar með, og hvað þau þýða.",
"tags-tag": "Heiti merkis",
"logentry-delete-delete": "$1 {{GENDER:$2|eyddi}} síðunni $3",
"logentry-delete-delete_redir": "$1 {{GENDER:$2|eyddi}} tilvísun $3 með því að yfirskrifa",
"logentry-delete-restore": "$1 {{GENDER:$2|endurvakti}} síðu $3 ($4)",
+ "restore-count-files": "{{PLURAL:$1|1 skrá|$1 skrár}}",
"logentry-delete-event": "$1 {{GENDER:$2|breytti}} sýnileika {{PLURAL:$5|færslu|$5 færslna}} á $3: $4",
"logentry-delete-revision": "$1 {{GENDER:$2|breytti}} sýnileika {{PLURAL:$5|útgáfu|$5 útgáfna}} á $3: $4",
"logentry-delete-event-legacy": "$1 {{GENDER:$2|breytti}} sýnileika færslna á $3",
"sessionprovider-mediawiki-session-cookiesessionprovider": "setur með vefkökum",
"sessionprovider-nocookies": "Vefkökur gætu verið óvirkar. Gakktu úr skugga um að smákökur séu virkar og byrjaðu svo aftur.",
"randomrootpage": "Handahófsvalin rótarsíða",
- "log-action-filter-all": "Allt"
+ "log-action-filter-all": "Allt",
+ "log-action-filter-delete-delete": "Eyðing síðu"
}
"rcfilters-group-results-by-page": "ページごとにまとめて表示",
"rcfilters-activefilters": "絞り込み",
"rcfilters-advancedfilters": "詳細フィルター",
- "rcfilters-limit-title": "表示件数の変更",
- "rcfilters-limit-and-date-label": "$1件の変更、$2",
+ "rcfilters-limit-title": "表示する件数",
+ "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|件の変更}}、$2",
"rcfilters-date-popup-title": "検索期間",
"rcfilters-days-title": "日数",
"rcfilters-hours-title": "時間",
"rcfilters-filter-watchlist-watchednew-description": "ウォッチリストに登録されていて、前回訪れた後に更新があったページ。",
"rcfilters-filter-watchlist-notwatched-label": "ウォッチリスト登録外",
"rcfilters-filter-watchlist-notwatched-description": "ウォッチリストに登録されているページ以外の全ての変更。",
- "rcfilters-filter-watchlistactivity-unseen-label": "保存していません!",
+ "rcfilters-filter-watchlistactivity-unseen-label": "未読の変更",
"rcfilters-filter-watchlistactivity-unseen-description": "ウォッチリストに登録されていて、前回訪れた後に更新があったページ。",
- "rcfilters-filter-watchlistactivity-seen-label": "最近の更新",
+ "rcfilters-filter-watchlistactivity-seen-label": "閲覧済みの変更",
"rcfilters-filtergroup-changetype": "変更の種類",
"rcfilters-filter-pageedits-label": "ページの編集",
"rcfilters-filter-pageedits-description": "ウィキの本文、議論、カテゴリの説明などの編集",
"rcfilters-preference-label": "最近の更新の改善版を隠す",
"rcfilters-preference-help": "2017年のインターフェース更新、当時追加したや以来の新しいツールの使用を断る。",
"rcfilters-filter-showlinkedfrom-label": "リンク先ページの変更を表示する",
- "rcfilters-target-page-placeholder": "ページ名を入力",
+ "rcfilters-target-page-placeholder": "ページ名(またはカテゴリ名)を入力",
"rcnotefrom": "以下は<strong>$3 $4</strong>以降の{{PLURAL:$5|更新です}} (最大 <strong>$1</strong> 件)。",
"rclistfromreset": "日時指定をリセット",
"rclistfrom": "$3の$2以降の更新を表示する",
"search-nonefound-thiswiki": "이 사이트에서 검색어와 일치하는 결과가 없습니다.",
"powersearch-legend": "고급 검색",
"powersearch-ns": "다음 이름공간에서 검색:",
- "powersearch-togglelabel": "확인:",
+ "powersearch-togglelabel": "선택:",
"powersearch-toggleall": "모두",
"powersearch-togglenone": "모두 제외",
"powersearch-remember": "향후 검색에 선택 기억하기",
"unwatchthispage": "주시 해제하기",
"notanarticle": "문서가 아님",
"notvisiblerev": "이 판은 삭제되었습니다.",
- "watchlist-details": "{{PLURAL:$1|문서 $1개}}가 주시문서 목록에 있습니다. (토론 문서 포함)",
+ "watchlist-details": "{{PLURAL:$1|문서 $1개}}가 주시문서 목록에 있습니다 (토론 문서 포함).",
"wlheader-enotif": "이메일 알림 기능이 활성화되었습니다.",
"wlheader-showupdated": "마지막으로 방문한 이후에 바뀐 문서는 '''굵은 글씨'''로 보입니다.",
"wlnote": "$3 $4 기준으로, 아래에 최근 {{PLURAL:$2|한 시간|<strong>$2</strong>시간}} 동안 {{PLURAL:$1|마지막 바뀜이|마지막 바뀜 <strong>$1</strong>개가}} 있습니다.",
"logentry-move-move_redir": "$1님이 $3 문서를 $4 문서로 {{GENDER:$2|이동하면서}} 넘겨주기를 덮어썼습니다",
"logentry-move-move_redir-noredirect": "$1님이 $3 문서를 $4 문서로 넘겨주기를 남기지 않고 {{GENDER:$2|이동하면서}} 이동할 대상에 있던 넘겨주기를 덮어썼습니다",
"logentry-patrol-patrol": "$1님이 $3 문서의 $4판을 점검한 것으로 {{GENDER:$2|표시했습니다}}",
- "logentry-patrol-patrol-auto": "$1ë\8b\98ì\9d´ ì\9e\90ë\8f\99ì \81ì\9c¼ë¡\9c $3 문ì\84\9cì\9d\98 $4í\8c\90ì\9d\84 ì \90ê²\80í\95\9c ê²\83ì\9c¼ë¡\9c {{GENDER:$2|í\91\9cì\8b\9cí\96\88ì\8aµë\8b\88ë\8b¤}}",
+ "logentry-patrol-patrol-auto": "$1님이 자동으로 $3 문서의 $4판을 점검한 것으로 {{GENDER:$2|표시했습니다}}",
"logentry-newusers-newusers": "$1 사용자 계정을 {{GENDER:$2|만들었습니다}}",
"logentry-newusers-create": "$1 사용자 계정을 {{GENDER:$2|만들었습니다}}",
"logentry-newusers-create2": "$3 사용자 계정을 $1님이 {{GENDER:$2|만들었습니다}}",
"logentry-protect-modify-cascade": "$1님이 $3 문서의 보호 수준을 {{GENDER:$2|바꾸었습니다}} $4 [연쇄적]",
"logentry-rights-rights": "$1님이 {{GENDER:$6|$3}}님의 권한을 $4에서 $5(으)로 {{GENDER:$2|바꾸었습니다}}",
"logentry-rights-rights-legacy": "$1님이 $3님의 권한을 {{GENDER:$2|바꾸었습니다}}",
- "logentry-rights-autopromote": "$1ë\8b\98ì\9d´ ê¶\8cí\95\9cì\9d\84 ì\9e\90ë\8f\99ì \81ì\9c¼ë¡\9c $4ì\97\90ì\84\9c $5(ì\9c¼)ë¡\9c {{GENDER:$2|ë°\94꾸ì\97\88ì\8aµë\8b\88ë\8b¤}}",
+ "logentry-rights-autopromote": "$1님이 권한을 자동으로 $4에서 $5(으)로 {{GENDER:$2|바꾸었습니다}}",
"logentry-upload-upload": "$1님이 $3 파일을 {{GENDER:$2|올렸습니다}}",
"logentry-upload-overwrite": "$1님이 $3의 새 판을 {{GENDER:$2|올렸습니다}}",
"logentry-upload-revert": "$1님이 $3 파일을 {{GENDER:$2|올렸습니다}}",
"booksources-search": "Zeuken",
"booksources-text": "Hieronder steet n lieste mit verwiezingen naor aandere websteeën die nieje of wat ouwere boeken verkopen, en daor hebben ze warschienlik meer informasie over t boek da'j zeuken:",
"booksources-invalid-isbn": "De op-egeven ISBN klop niet; kiek effen nao o'j gien fout emaakt hebben bie de invoer.",
+ "magiclink-tracking-isbn": "Ziejen die magiese ISBN-verwiezingen gebruken",
"specialloguserlabel": "Uutvoerende gebruker:",
"speciallogtitlelabel": "Doel (ziednaam of gebruker):",
"log": "Logboeken",
"rcfilters-activefilters": "Filtros ativos",
"rcfilters-advancedfilters": "Filtros avançados",
"rcfilters-limit-title": "Resultados para mostrar",
- "rcfilters-limit-and-date-label": "{{PLURAL:$1|mudança|$1 mudanças}}, $2",
+ "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|mudança|mudanças}}, $2",
"rcfilters-date-popup-title": "Período de tempo para pesquisar",
"rcfilters-days-title": "Dias recentes",
"rcfilters-hours-title": "Horas recentes",
"compare-title-not-exists": "O título que especificou não existe.",
"compare-revision-not-exists": "A revisão que especificou não existe.",
"diff-form": "Diferenças",
- "diff-form-oldid": "Identificador de revisão antigo (opcional)",
- "diff-form-revid": "Identificador de revisão da diferença",
+ "diff-form-oldid": "Identificador da revisão anterior (opcional)",
+ "diff-form-revid": "Identificador da revisão a comparar",
"diff-form-submit": "Mostrar diferenças",
"permanentlink": "Hiperligação permanente",
"permanentlink-revid": "Identificador de revisão",
"rcfilters-filter-watchlist-notwatched-label": "Label for the filter for showing changes to pages not on your watchlist.",
"rcfilters-filter-watchlist-notwatched-description": "Description for the filter for showing changes to pages not on your watchlist.",
"rcfilters-filtergroup-watchlistactivity": "Title for the watchlist activity filter group (only available on [[Special:Watchlist]])",
- "rcfilters-filter-watchlistactivity-unseen-label": "Label for unseen changes in the watchlist activity filter group.",
- "rcfilters-filter-watchlistactivity-unseen-description": "Description for unseen changes in the watchlist activity filter group.",
- "rcfilters-filter-watchlistactivity-seen-label": "Label for seen changes in the watchlist activity filter group.",
- "rcfilters-filter-watchlistactivity-seen-description": "Description for seen changes in the watchlist activity filter group.",
+ "rcfilters-filter-watchlistactivity-unseen-label": "Label for unseen changes in the watchlist activity filter group.\n\n{{Related|Rcfilters-filter-watchlistactivity}}",
+ "rcfilters-filter-watchlistactivity-unseen-description": "Description for unseen changes in the watchlist activity filter group.\n\n{{Related|Rcfilters-filter-watchlistactivity}}",
+ "rcfilters-filter-watchlistactivity-seen-label": "Label for seen changes in the watchlist activity filter group.\n\n{{Related|Rcfilters-filter-watchlistactivity}}",
+ "rcfilters-filter-watchlistactivity-seen-description": "Description for seen changes in the watchlist activity filter group.\n\n{{Related|Rcfilters-filter-watchlistactivity}}",
"rcfilters-filtergroup-changetype": "Title for the filter group for edit type.",
"rcfilters-filter-pageedits-label": "Label for the filter for showing the edits to existing pages.",
"rcfilters-filter-pageedits-description": "Description for the filter for showing edits to existing pages.",
"rcfilters-filter-editsbyself-label": "مون پاران تبديليون",
"rcfilters-filter-editsbyself-description": "توھان جون پنھنجون ڀاڱيداريون.",
"rcfilters-filter-editsbyother-label": "ٻين پاران تبديليون",
+ "rcfilters-filtergroup-userExpLevel": "واپرائيندڙن جي داخلا ۽ تجربو",
"rcfilters-filter-user-experience-level-registered-label": "رجسٽر ٿيل",
"rcfilters-filter-user-experience-level-registered-description": "داخل ٿيل ايڊيٽر.",
"rcfilters-filter-user-experience-level-unregistered-label": "اڻرجسٽر ٿيل",
+ "rcfilters-filter-user-experience-level-unregistered-description": "سنواريندڙ جيڪي داخل ٿيل ناھن.",
"rcfilters-filter-user-experience-level-newcomer-label": "نوان ايندڙ",
"rcfilters-filter-user-experience-level-learner-label": "سکندڙ",
"rcfilters-filter-user-experience-level-experienced-label": "تجربيڪار واپرائيندڙ",
"enotif_lastdiff": "Да видите ову измену, погледајте $1.",
"enotif_anon_editor": "анониман корисник $1",
"enotif_body": "Поштовани $WATCHINGUSERNAME,\n \t\n$PAGEINTRO $NEWPAGE\n\nОпис: $PAGESUMMARY $PAGEMINOREDIT\n\nКонтакт:\nмејл: $PAGEEDITOR_EMAIL\nвики: $PAGEEDITOR_WIKI\n\nНеће бити других обавештења у случају даљих измена уколико не посетите ову страницу када сте пријављени.\nМожете и да поништите поставке обавештења за све странице у вашем списку надгледања.\n\nСрдачан поздрав, {{SITENAME}}\n\n--\nДа бисте променили поставке имејл обавештења, посетите\n{{canonicalurl:{{#special:Preferences}}}}\n\nДа бисте променили поставке списка надгледања, посетите\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nДа бисте уклонили ову страницу са списка надгледања, посетите\n$UNWATCHURL\n\nПодршка и даља помоћ:\n$HELPPAGE",
+ "enotif_minoredit": "Ово је мања измена",
"created": "направљена",
"changed": "измењена",
"deletepage": "Обриши страницу",
"removecredentials": "Уклањање акредитива",
"credentialsform-provider": "Врста акредитива:",
"credentialsform-account": "Назив налога:",
+ "userjsispublic": "Напомена: JavaScript подстранице не би требале садржавати поверљиве информације будући да су видљиве другим корисницима.",
+ "usercssispublic": "Напомена: CSS подстранице не би требале садржавати поверљиве информације будући да су видљиве другим корисницима.",
"rawhtml-notallowed": "<html> тагови не могу да се користе ван нормалних страница.",
"gotointerwiki": "Напуштам пројекат {{SITENAME}}",
"gotointerwiki-invalid": "Одабрани наслов је невалидан.",
"feedback-termsofuse": "Prihvatam da pošaljem povratne informacije u skladu sa uslovima korišćenja.",
"feedback-thanks": "Hvala! Vaša povratna informacija je postavljena na stranicu „[$2 $1]“.",
"feedback-thanks-title": "Hvala vam!",
- "searchsuggest-search": "Pretraga projekta {{SITENAME}}",
+ "searchsuggest-search": "Pretraga",
"searchsuggest-containing": "sadrži...",
"api-error-badtoken": "Unutrašnja greška: neispravan žeton.",
"api-error-emptypage": "Stvaranje novih praznih stranica nije dozvoljeno.",
"shown-title": "فی صفحہ $1 {{PLURAL:$1|نتیجہ|نتائج}} دکھائیں",
"viewprevnext": "($1 {{int:pipe-separator}} $2) دیکھیں ($3)",
"searchmenu-exists": "<strong>اِس ویکی پر «[[:$1]]» نامی ایک صفحہ موجود ہے۔</strong> {{PLURAL:$2|0=|تلاش کے دیگر نتائج بھی ملاحظہ فرمائیں۔}}",
- "searchmenu-new": "<strong>صÙ\81ØÛ\81 \"[[:$1]]\" Ú©Ù\88 اس Ù\88Û\8cÚ©Û\8c پر تخÙ\84Û\8cÙ\82 کرÛ\8cÚº</strong> {{PLURAL:$2|0=|Ù\88Û\81 صÙ\81ØÛ\81 بھÛ\8c دÛ\8cÚ©Ú¾Û\92 جÙ\88 Ù\93Ù¾ Ú©Û\92 تÙ\84اش Ù\85Û\8cÚº پاÛ\8cا Ú¯Û\8cا|اÙ\86 Ù\86تائج Ú©Ù\88 بھÛ\8c دÛ\8cÚ©Ú¾Û\92 جÙ\88 پائÛ\92 گئÛ\92}}",
+ "searchmenu-new": "<strong>صÙ\81ØÛ\81 \"[[:$1]]\" Ú©Ù\88 اس Ù\88Û\8cÚ©Û\8c پر تخÙ\84Û\8cÙ\82 کرÛ\8cÚº</strong> {{PLURAL:$2|0=|Ù\88Û\81 صÙ\81ØÛ\81 بھÛ\8c دÛ\8cÚ©Ú¾Û\8cÚº جÙ\88 تÙ\84اش Ù\85Û\8cÚº پاÛ\8cا Ú¯Û\8cا|اÙ\86 Ù\86تائج Ú©Ù\88 بھÛ\8c دÛ\8cÚ©Ú¾Û\8cÚº جÙ\88 پائÛ\92 گئÛ\92Û\94}}",
"searchprofile-articles": "مواد کے حامل صفحات",
"searchprofile-images": "ملٹی میڈیا",
"searchprofile-everything": "سب کچھ",
expandFrom = 'left';
// Catch invalid values, default to 'auto'
- } else if ( $.inArray( expandFrom, [ 'left', 'right', 'start', 'end', 'auto' ] ) === -1 ) {
+ } else if ( [ 'left', 'right', 'start', 'end', 'auto' ].indexOf( expandFrom ) === -1 ) {
expandFrom = 'auto';
}
];
if ( context.data.keypressedCount === 0 &&
e.which === context.data.keypressed &&
- $.inArray( e.which, allowed ) !== -1
+ allowed.indexOf( e.which ) !== -1
) {
$.suggestions.keypress( e, context, context.data.keypressed );
}
function uniqueElements( array ) {
var uniques = [];
- $.each( array, function ( i, elem ) {
- if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
+ array.forEach( function ( elem ) {
+ if ( elem !== undefined && uniques.indexOf( elem ) === -1 ) {
uniques.push( elem );
}
} );
cell = this;
// Get current column index
columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
- newSortList = $.map( columns, function ( c ) {
- // jQuery "helpfully" flattens the arrays...
- return [ [ c, $cell.data( 'order' ) ] ];
+ newSortList = columns.map( function ( c ) {
+ return [ c, $cell.data( 'order' ) ];
} );
// Index of first column belonging to this header
i = columns[ 0 ];
mw.util.getParamValue( 'undo' ) !== null ||
// Pressing "show changes" and "preview" also signify that the user will
// probably save the page soon
- $.inArray( $form.find( '#mw-edit-mode' ).val(), [ 'preview', 'diff' ] ) > -1
+ [ 'preview', 'diff' ].indexOf( $form.find( '#mw-edit-mode' ).val() ) > -1
) {
checkStash();
}
for ( j = 0; j < tmp.length; j++ ) {
availableFormats[ tmp[ j ] ] = true;
}
- pi.parameters[ i ].type = $.grep( tmp, filterFmModules );
+ pi.parameters[ i ].type = tmp.filter( filterFmModules );
pi.parameters[ i ][ 'default' ] = 'json';
pi.parameters[ i ].required = true;
}
// Hide the 'wrappedhtml' parameter on format modules
if ( pi.group === 'format' ) {
- pi.parameters = $.grep( pi.parameters, function ( p ) {
+ pi.parameters = pi.parameters.filter( function ( p ) {
return p.name !== 'wrappedhtml';
} );
}
popup: {
width: 'auto',
padded: true,
- $content: $( '<ul>' ).append( $.map( pi.helpurls, function ( link ) {
+ $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
return $( '<li>' ).append( $( '<a>' )
.attr( { href: link, target: '_blank' } )
.text( link )
popup: {
width: 'auto',
padded: true,
- $content: $( '<ul>' ).append( $.map( pi.examples, function ( example ) {
+ $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
var a = $( '<a>' )
.attr( 'href', '#' + example.query )
.html( example.description );
expiryValue = expiryWidget.dropdowninput.getValue(),
// infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
- isIndefinite = $.inArray( expiryValue, infinityValues ) !== -1 ||
- ( expiryValue === 'other' && $.inArray( expiryWidget.textinput.getValue(), infinityValues ) !== -1 );
+ isIndefinite = infinityValues.indexOf( expiryValue ) !== -1 ||
+ ( expiryValue === 'other' && infinityValues.indexOf( expiryWidget.textinput.getValue() ) !== -1 );
if ( enableAutoblockField ) {
enableAutoblockField.toggle( !( isNonEmptyIp ) );
if (
mw.config.get( 'wgCheckFileExtensions' ) &&
mw.config.get( 'wgStrictFileExtensions' ) &&
- mw.config.get( 'wgFileExtensions' ) &&
+ Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
$( this ).attr( 'id' ) !== 'wpUploadFileURL'
) {
if (
fname.lastIndexOf( '.' ) === -1 ||
- $.inArray(
- fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase(),
- $.map( mw.config.get( 'wgFileExtensions' ), function ( element ) {
- return element.toLowerCase();
- } )
- ) === -1
+ mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
+ return element.toLowerCase();
+ } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
) {
// Not a valid extension
// Clear the upload and set mw-upload-permitted to error
function fileIsPreviewable( file ) {
var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
tooHuge = 10 * 1024 * 1024;
- return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
+ return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
}
/**
'import',
'options'
];
- if ( $.inArray( action, csrfActions ) !== -1 ) {
+ if ( csrfActions.indexOf( action ) !== -1 ) {
mw.track( 'mw.deprecate', 'apitoken_' + action );
mw.log.warn( 'Use of the "' + action + '" token is deprecated. Use "csrf" instead.' );
return 'csrf';
startTagName = startTagName.toLowerCase();
endTagName = endTagName.toLowerCase();
- if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) {
+ if ( startTagName !== endTagName || settings.allowedHtmlElements.indexOf( startTagName ) === -1 ) {
return false;
}
for ( i = 0, len = attributes.length; i < len; i += 2 ) {
attributeName = attributes[ i ];
- if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 &&
- $.inArray( attributeName, settings.allowedHtmlAttributesByElement[ startTagName ] || [] ) === -1 ) {
+ if ( settings.allowedHtmlCommonAttributes.indexOf( attributeName ) === -1 &&
+ ( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).indexOf( attributeName ) === -1 ) {
return false;
}
}
// Try the cache
if ( cache[ url ] ) {
// Update access freshness
- cacheOrder.splice( $.inArray( url, cacheOrder ), 1 );
+ cacheOrder.splice( cacheOrder.indexOf( url ), 1 );
cacheOrder.push( url );
return $.Deferred().resolve( cache[ url ] ).promise();
}
* @return array
*/
private function listTables() {
+ global $wgCommentTableSchemaMigrationStage;
+
$tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
'archive', 'user_groups', 'page_props', 'category'
];
+ if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ // The new tables for comments are in use
+ $tables[] = 'comment';
+ $tables[] = 'revision_comment_temp';
+ $tables[] = 'image_comment_temp';
+ }
+
if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) {
array_push( $tables, 'searchindex' );
}
<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8">
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="2"/>
-</svg>
+ <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:title="?>">test</a>
+</svg>
\ No newline at end of file
use Exception;
use HashBagOStuff;
use InvalidArgumentException;
+use Language;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\BlobStoreFactory;
use MediaWiki\Storage\IncompleteRevisionException;
use MediaWiki\Storage\MutableRevisionRecord;
use MediaWiki\Storage\RevisionRecord;
*/
public function testNewRevisionFromRow_anonEdit() {
$page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ $text = __METHOD__ . 'a-ä';
/** @var Revision $rev */
$rev = $page->doEditContent(
- new WikitextContent( __METHOD__. 'a' ),
+ new WikitextContent( $text ),
__METHOD__. 'a'
)->value['revision'];
$page->getTitle()
);
$this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
+ */
+ public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
+ $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+ $this->overrideMwServices();
+ $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ $text = __METHOD__ . 'a-ä';
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( $text ),
+ __METHOD__. 'a'
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->newRevisionFromRow(
+ $this->revisionToRow( $rev ),
+ [],
+ $page->getTitle()
+ );
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
}
/**
*/
public function testNewRevisionFromRow_userEdit() {
$page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ $text = __METHOD__ . 'b-ä';
/** @var Revision $rev */
$rev = $page->doEditContent(
- new WikitextContent( __METHOD__. 'b' ),
+ new WikitextContent( $text ),
__METHOD__ . 'b',
0,
false,
$page->getTitle()
);
$this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
}
/**
public function testNewRevisionFromArchiveRow() {
$store = MediaWikiServices::getInstance()->getRevisionStore();
$title = Title::newFromText( __METHOD__ );
+ $text = __METHOD__ . '-bä';
+ $page = WikiPage::factory( $title );
+ /** @var Revision $orig */
+ $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
+ ->value['revision'];
+ $page->doDeleteArticle( __METHOD__ );
+
+ $db = wfGetDB( DB_MASTER );
+ $arQuery = $store->getArchiveQueryInfo();
+ $res = $db->select(
+ $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+ __METHOD__, [], $arQuery['joins']
+ );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+ $record = $store->newRevisionFromArchiveRow( $row );
+
+ $this->assertRevisionRecordMatchesRevision( $orig, $record );
+ $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
+ */
+ public function testNewRevisionFromArchiveRow_legacyEncoding() {
+ $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+ $this->overrideMwServices();
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $title = Title::newFromText( __METHOD__ );
+ $text = __METHOD__ . '-bä';
$page = WikiPage::factory( $title );
/** @var Revision $orig */
- $orig = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
->value['revision'];
$page->doDeleteArticle( __METHOD__ );
$record = $store->newRevisionFromArchiveRow( $row );
$this->assertRevisionRecordMatchesRevision( $orig, $record );
+ $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
}
/**
'content' => new WikitextContent( 'Some Content' ),
]
];
+ yield 'Basic array, serialized text' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+ ]
+ ];
+ yield 'Basic array, serialized text, utf-8 flags' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+ 'flags' => 'utf-8',
+ ]
+ ];
yield 'Basic array, with title' => [
[
'title' => Title::newFromText( 'SomeText' ),
$this->assertTrue(
$result->getSlot( 'main' )->getContent()->equals( $array['content'] )
);
+ } elseif ( isset( $array['text'] ) ) {
+ $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() );
} else {
$this->assertSame(
$array['content_format'],
}
}
+ /**
+ * @dataProvider provideNewMutableRevisionFromArray
+ * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ */
+ public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ $blobStore = new SqlBlobStore( wfGetLB(), $cache );
+ $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+ $factory = $this->getMockBuilder( BlobStoreFactory::class )
+ ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $factory->expects( $this->any() )
+ ->method( 'newBlobStore' )
+ ->willReturn( $blobStore );
+ $factory->expects( $this->any() )
+ ->method( 'newSqlBlobStore' )
+ ->willReturn( $blobStore );
+
+ $this->setService( 'BlobStoreFactory', $factory );
+
+ $this->testNewMutableRevisionFromArray( $array );
+ }
+
}
namespace MediaWiki\Tests\Storage;
+use HashBagOStuff;
+use Language;
use MediaWiki\Storage\RevisionAccessException;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\SqlBlobStore;
use MediaWikiTestCase;
+use Title;
use WANObjectCache;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\LoadBalancer;
$store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
}
- // FIXME: test getRevisionSizes
+ public function provideNewRevisionFromRow_legacyEncoding_applied() {
+ yield 'windows-1252, old_flags is empty' => [
+ 'windows-1252',
+ 'en',
+ [
+ 'old_flags' => '',
+ 'old_text' => "S\xF6me Content",
+ ],
+ 'Söme Content'
+ ];
+
+ yield 'windows-1252, old_flags is null' => [
+ 'windows-1252',
+ 'en',
+ [
+ 'old_flags' => null,
+ 'old_text' => "S\xF6me Content",
+ ],
+ 'Söme Content'
+ ];
+ }
+
+ /**
+ * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
+ *
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
+ */
+ public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+
+ $blobStore = new SqlBlobStore( wfGetLB(), $cache );
+ $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
+
+ $store = new RevisionStore( wfGetLB(), $blobStore, $cache );
+
+ $record = $store->newRevisionFromRow(
+ $this->makeRow( $row ),
+ 0,
+ Title::newFromText( __METHOD__ . '-UTPage' )
+ );
+
+ $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
+ */
+ public function testNewRevisionFromRow_legacyEncoding_ignored() {
+ $row = [
+ 'old_flags' => 'utf-8',
+ 'old_text' => 'Söme Content',
+ ];
+
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+
+ $blobStore = new SqlBlobStore( wfGetLB(), $cache );
+ $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+ $store = new RevisionStore( wfGetLB(), $blobStore, $cache );
+
+ $record = $store->newRevisionFromRow(
+ $this->makeRow( $row ),
+ 0,
+ Title::newFromText( __METHOD__ . '-UTPage' )
+ );
+ $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
+ }
+
+ private function makeRow( array $array ) {
+ $row = $array + [
+ 'rev_id' => 7,
+ 'rev_page' => 5,
+ 'rev_text_id' => 11,
+ 'rev_timestamp' => '20110101000000',
+ 'rev_user_text' => 'Tester',
+ 'rev_user' => 17,
+ 'rev_minor_edit' => 0,
+ 'rev_deleted' => 0,
+ 'rev_len' => 100,
+ 'rev_parent_id' => 0,
+ 'rev_sha1' => 'deadbeef',
+ 'rev_comment_text' => 'Testing',
+ 'rev_comment_data' => '{}',
+ 'rev_comment_cid' => 111,
+ 'rev_content_format' => CONTENT_FORMAT_TEXT,
+ 'rev_content_model' => CONTENT_MODEL_TEXT,
+ 'page_namespace' => 0,
+ 'page_title' => 'TEST',
+ 'page_id' => 5,
+ 'page_latest' => 7,
+ 'page_is_redirect' => 0,
+ 'page_len' => 100,
+ 'user_name' => 'Tester',
+ 'old_is' => 13,
+ 'old_text' => 'Hello World',
+ 'old_flags' => 'utf-8',
+ ];
+
+ return (object)$row;
+ }
}
false // retry
],
],
+ '200 OK - Directory with non-array "nodes" key' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ 'nodes' => 'not an array'
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => [
+ null,
+ "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
+ false // retry
+ ],
+ ],
'200 OK - Correctly encoded garbage response' => [
'http' => [
'code' => 200,
*/
public function testChronologyProtector() {
// (a) First HTTP request
- $mPos = new MySQLMasterPos( 'db1034-bin.000976', '843431247' );
+ $m1Pos = new MySQLMasterPos( 'db1034-bin.000976', '843431247' );
+ $m2Pos = new MySQLMasterPos( 'db1064-bin.002400', '794074907' );
$now = microtime( true );
- $mockDB = $this->getMockBuilder( 'DatabaseMysqli' )
+
+ // Master DB 1
+ $mockDB1 = $this->getMockBuilder( 'DatabaseMysqli' )
->disableOriginalConstructor()
->getMock();
- $mockDB->method( 'writesOrCallbacksPending' )->willReturn( true );
- $mockDB->method( 'lastDoneWrites' )->willReturn( $now );
- $mockDB->method( 'getMasterPos' )->willReturn( $mPos );
-
- $lb = $this->getMockBuilder( 'LoadBalancer' )
+ $mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true );
+ $mockDB1->method( 'lastDoneWrites' )->willReturn( $now );
+ $mockDB1->method( 'getMasterPos' )->willReturn( $m1Pos );
+ // Load balancer for master DB 1
+ $lb1 = $this->getMockBuilder( 'LoadBalancer' )
->disableOriginalConstructor()
->getMock();
- $lb->method( 'getConnection' )->willReturn( $mockDB );
- $lb->method( 'getServerCount' )->willReturn( 2 );
- $lb->method( 'parentInfo' )->willReturn( [ 'id' => "main-DEFAULT" ] );
- $lb->method( 'getAnyOpenConnection' )->willReturn( $mockDB );
- $lb->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback(
- function () use ( $mockDB ) {
+ $lb1->method( 'getConnection' )->willReturn( $mockDB1 );
+ $lb1->method( 'getServerCount' )->willReturn( 2 );
+ $lb1->method( 'getAnyOpenConnection' )->willReturn( $mockDB1 );
+ $lb1->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback(
+ function () use ( $mockDB1 ) {
$p = 0;
- $p |= call_user_func( [ $mockDB, 'writesOrCallbacksPending' ] );
- $p |= call_user_func( [ $mockDB, 'lastDoneWrites' ] );
+ $p |= call_user_func( [ $mockDB1, 'writesOrCallbacksPending' ] );
+ $p |= call_user_func( [ $mockDB1, 'lastDoneWrites' ] );
return (bool)$p;
}
) );
- $lb->method( 'getMasterPos' )->willReturn( $mPos );
+ $lb1->method( 'getMasterPos' )->willReturn( $m1Pos );
+ $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
+ // Master DB 2
+ $mockDB2 = $this->getMockBuilder( 'DatabaseMysqli' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true );
+ $mockDB2->method( 'lastDoneWrites' )->willReturn( $now );
+ $mockDB2->method( 'getMasterPos' )->willReturn( $m2Pos );
+ // Load balancer for master DB 2
+ $lb2 = $this->getMockBuilder( 'LoadBalancer' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lb2->method( 'getConnection' )->willReturn( $mockDB2 );
+ $lb2->method( 'getServerCount' )->willReturn( 2 );
+ $lb2->method( 'getAnyOpenConnection' )->willReturn( $mockDB2 );
+ $lb2->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback(
+ function () use ( $mockDB2 ) {
+ $p = 0;
+ $p |= call_user_func( [ $mockDB2, 'writesOrCallbacksPending' ] );
+ $p |= call_user_func( [ $mockDB2, 'lastDoneWrites' ] );
+
+ return (bool)$p;
+ }
+ ) );
+ $lb2->method( 'getMasterPos' )->willReturn( $m2Pos );
+ $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
$bag = new HashBagOStuff();
$cp = new ChronologyProtector(
]
);
- $mockDB->expects( $this->exactly( 2 ) )->method( 'writesOrCallbacksPending' );
- $mockDB->expects( $this->exactly( 2 ) )->method( 'lastDoneWrites' );
+ $mockDB1->expects( $this->exactly( 1 ) )->method( 'writesOrCallbacksPending' );
+ $mockDB1->expects( $this->exactly( 1 ) )->method( 'lastDoneWrites' );
+ $mockDB2->expects( $this->exactly( 1 ) )->method( 'writesOrCallbacksPending' );
+ $mockDB2->expects( $this->exactly( 1 ) )->method( 'lastDoneWrites' );
+
+ // Nothing to wait for on first HTTP request start
+ $cp->initLB( $lb1 );
+ $cp->initLB( $lb2 );
+ // Record positions in stash on first HTTP request end
+ $cp->shutdownLB( $lb1 );
+ $cp->shutdownLB( $lb2 );
+ $cpIndex = null;
+ $cp->shutdown( null, 'sync', $cpIndex );
- // Nothing to wait for
- $cp->initLB( $lb );
- // Record in stash
- $cp->shutdownLB( $lb );
- $cp->shutdown();
+ $this->assertEquals( 1, $cpIndex, "CP write index set" );
// (b) Second HTTP request
+
+ // Load balancer for master DB 1
+ $lb1 = $this->getMockBuilder( 'LoadBalancer' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lb1->method( 'getServerCount' )->willReturn( 2 );
+ $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
+ $lb1->expects( $this->once() )
+ ->method( 'waitFor' )->with( $this->equalTo( $m1Pos ) );
+ // Load balancer for master DB 2
+ $lb2 = $this->getMockBuilder( 'LoadBalancer' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lb2->method( 'getServerCount' )->willReturn( 2 );
+ $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
+ $lb2->expects( $this->once() )
+ ->method( 'waitFor' )->with( $this->equalTo( $m2Pos ) );
+
$cp = new ChronologyProtector(
$bag,
[
'ip' => '127.0.0.1',
'agent' => "Totally-Not-FireFox"
- ]
+ ],
+ $cpIndex
);
- $lb->expects( $this->once() )
- ->method( 'waitFor' )->with( $this->equalTo( $mPos ) );
+ // Wait for last positions to be reached on second HTTP request start
+ $cp->initLB( $lb1 );
+ $cp->initLB( $lb2 );
+ // Shutdown (nothing to record)
+ $cp->shutdownLB( $lb1 );
+ $cp->shutdownLB( $lb2 );
+ $cpIndex = null;
+ $cp->shutdown( null, 'sync', $cpIndex );
- // Wait
- $cp->initLB( $lb );
- // Record in stash
- $cp->shutdownLB( $lb );
- $cp->shutdown();
+ $this->assertEquals( null, $cpIndex, "CP write index retained" );
}
private function newLBFactoryMulti( array $baseOverride = [], array $serverOverride = [] ) {
// data: URIs for red.gif, green.gif, circle.svg
$red = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs=';
$green = 'data:image/gif;base64,R0lGODlhAQABAIAAAACAADAAACwAAAAAAQABAAACAkQBADs=';
- $svg = 'data:image/svg+xml,%3C%3Fxml version=%221.0%22 encoding=%22UTF-8%22%3F%3E '
- . '%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%228%22 height='
- . '%228%22%3E %3Ccircle cx=%224%22 cy=%224%22 r=%222%22/%3E %3C/svg%3E';
+ $svg = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%228'
+ . '%22 height=%228%22 viewBox=%220 0 8 8%22%3E %3Ccircle cx=%224%22 cy=%224%22 '
+ . 'r=%222%22/%3E %3Ca xmlns:xlink=%22http://www.w3.org/1999/xlink%22 xlink:title='
+ . '%22%3F%3E%22%3Etest%3C/a%3E %3C/svg%3E';
// phpcs:disable Generic.Files.LineLength
return [
/**
* @covers Wikimedia\Http\HttpAcceptNegotiator
*
- * @license GPL-2.0+
* @author Daniel Kinzler
*/
class HttpAcceptNegotiatorTest extends \PHPUnit_Framework_TestCase {
/**
* @covers Wikimedia\Http\HttpAcceptParser
*
- * @license GPL-2.0+
* @author Daniel Kinzler
*/
class HttpAcceptParserTest extends \PHPUnit_Framework_TestCase {
/**
* @covers Wikimedia\Rdbms\ConnectionManager
*
- * @license GPL-2.0+
* @author Daniel Kinzler
*/
class ConnectionManagerTest extends \PHPUnit_Framework_TestCase {
/**
* @covers Wikimedia\Rdbms\SessionConsistentConnectionManager
*
- * @license GPL-2.0+
* @author Daniel Kinzler
*/
class SessionConsistentConnectionManagerTest extends \PHPUnit_Framework_TestCase {
/**
* @covers PageDataRequestHandler
- *
* @group PageData
- *
- * @license GPL-2.0+
*/
class PageDataRequestHandlerTest extends \MediaWikiTestCase {
/**
* @covers SpecialPageData
- *
* @group Database
- *
* @group SpecialPage
*
- * @license GPL-2.0+
* @author Daniel Kinzler
*/
class SpecialPageDataTest extends SpecialPageTestBase {
/**
* @covers LanguageCode
- *
* @group Language
*
- * @license GPL-2.0+
* @author Thiemo Kreuz
*/
class LanguageCodeTest extends PHPUnit_Framework_TestCase {
function among( actual, expected, message ) {
if ( Array.isArray( expected ) ) {
- assert.ok( $.inArray( actual, expected ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' );
+ assert.ok( expected.indexOf( actual ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' );
} else {
assert.equal( actual, expected, message );
}
// Requests are POST, match requestBody instead of url
this.server.respond( function ( request ) {
- if ( $.inArray( request.requestBody, [
+ if ( [
// simple
'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C',
// two options
'action=options&format=json&formatversion=2&change=foo&token=%2B%5C',
// reset an option, not bundleable
'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C'
- ] ) !== -1 ) {
+ ].indexOf( request.requestBody ) !== -1 ) {
assert.ok( true, 'Repond to ' + request.requestBody );
request.respond( 200, { 'Content-Type': 'application/json' },
'{ "options": "success" }' );
// Requests are POST, match requestBody instead of url
this.server.respond( function ( request ) {
- if ( $.inArray( request.requestBody, [
+ if ( [
// simple
'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C',
// two options
'action=options&format=json&formatversion=2&change=foo&token=%2B%5C',
// reset an option, not bundleable
'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C'
- ] ) !== -1 ) {
+ ].indexOf( request.requestBody ) !== -1 ) {
assert.ok( true, 'Repond to ' + request.requestBody );
request.respond(
200,
}
function sequenceBodies( status, headers, bodies ) {
- jQuery.each( bodies, function ( i, body ) {
+ bodies.forEach( function ( body, i ) {
bodies[ i ] = [ status, headers, body ];
} );
return sequence( bodies );
]
];
- $.each( testCases, function () {
+ testCases.forEach( function ( testCase ) {
var
- key = this[ 0 ],
- input = this[ 1 ],
- output = this[ 2 ];
+ key = testCase[ 0 ],
+ input = testCase[ 1 ],
+ output = testCase[ 2 ];
mw.messages.set( key, input );
assert.htmlEqual(
formatParse( key ),
]
];
- $.each( testCases, function () {
+ testCases.forEach( function ( testCase ) {
var
- key = this[ 0 ],
- input = this[ 1 ],
- output = this[ 2 ],
+ key = testCase[ 0 ],
+ input = testCase[ 1 ],
+ output = testCase[ 2 ],
paramHref = key.slice( 0, 8 ) === 'wikilink' ? 'Example' : 'http://example.com',
paramText = 'Text';
mw.messages.set( key, input );